package com.framework.file.service.impl;

import com.alibaba.nacos.common.utils.UuidUtils;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import com.framework.common.exception.BusinessException;
import com.framework.common.util.AssertUtils;
import com.framework.common.util.BeanUtils;
import com.framework.common.util.ParamUtils;
import com.framework.common.util.StringUtils;
import com.framework.file.config.MinIoClientConfig;
import com.framework.file.dto.SysFileDto;
import com.framework.file.mapper.SysFileMapper;
import com.framework.file.model.SysFile;
import com.framework.file.param.SysFileParam;
import com.framework.file.repository.SysFileRepository;
import com.framework.file.service.ISysFileService;
import com.framework.file.vo.ChunkMergeVo;
import com.framework.file.vo.ChunkVo;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletContext;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Log4j2
@Service
public class SysFileServiceImpl implements ISysFileService {
    @Autowired
    private SysFileRepository baseRepo;

    @Autowired
    private SysFileMapper baseMapper;

    @Autowired
    private MinioClient minioClient;

    @Autowired
    private MinIoClientConfig minIoClientConfig;

    @Override
    @Transactional
    public String upload(SysFileDto sysFile) throws Exception {
        SysFile oldFile = new SysFile();
        sysFile.setBucketName(minIoClientConfig.getBucketName());
        BeanUtils.copyProperties(sysFile, oldFile);
        SysFile file = this.doUpload(sysFile);
        file.setCreateTime(new Date());
        file.setUpdateTime(new Date());

        baseRepo.save(file);
        return file.getId();
    }

    @Override
    public void uploadAndUpdate(SysFileDto sysFile, String oldFileId) throws Exception {
        SysFile oldFile = new SysFile();
        BeanUtils.copyProperties(sysFile, oldFile);
        oldFile.setUid(oldFileId);
        SysFile file = this.doUpload(sysFile);
        file.setUpdateTime(new Date());
        file.setId(oldFileId);
        // 上传更新成功后删除掉旧的文件
        deleteFileObject(oldFile);
        baseRepo.save(file);
    }

    @Override
    public void download(String id) throws Exception {
        SysFile sysFile = baseRepo.findOne(id);
        if (StringUtils.isEmpty(sysFile)) throw new BusinessException("未找到id:" + id + "数据");
        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(sysFile.getBucketName()).object(sysFile.getUid() + "." + sysFile.getExt()).build();
        //创建响应头
        HttpServletResponse response = this.createHeader(sysFile.getName());
        try (GetObjectResponse inputStream = minioClient.getObject(objectArgs)) {
            int len;
            //建立缓冲
            byte[] buffer = new byte[4096];
            try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()) {
                while ((len = inputStream.read(buffer)) != -1) {
                    os.write(buffer, 0, len);
                }
                os.flush();
                byte[] bytes = os.toByteArray();
                try (ServletOutputStream stream = response.getOutputStream()) {
                    stream.write(bytes);
                    stream.flush();
                }
                response.getOutputStream().flush();
                response.flushBuffer();
                response.getOutputStream().close();
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }
    }

    public InputStream getInputStream(SysFile sysFile) throws Exception {
        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(sysFile.getBucketName()).object(sysFile.getUid() + "." + sysFile.getExt()).build();
        GetObjectResponse inputStream = minioClient.getObject(objectArgs);
        sysFile.setSize(Long.valueOf(inputStream.available()));
        return inputStream;
    }

    @Override
    public SysFileDto getFileData(String id) throws Exception {
        SysFile sysFile = baseRepo.findOne(id);
        if (StringUtils.isEmpty(sysFile)) {
            throw new BusinessException("未找到id:" + id + "数据");
        }
        SysFileDto dto = BeanUtils.copyObject(sysFile, SysFileDto.class);
        try(InputStream ins = this.getInputStream(sysFile)) {
            byte[] byteArray = ByteStreams.toByteArray(ins);
            dto.setData(byteArray);
        }
        return dto;
    }


    @Override
    public void deleteFile(String id) {
        SysFile sysFile = baseRepo.findOne(id);
        if (sysFile == null) {
            throw new BusinessException("fileId为：" + id + "的数据未找到!删除失败!");
        }
        if (StringUtils.isBlank(sysFile.getBucketName())) {
            baseRepo.deleteById(id);
            return;
        }
        try {
            Object obj = this.doRemoveObjects(sysFile.getBucketName(), Arrays.asList(sysFile.getUid() + "." + sysFile.getExt()));
            if (obj != null) {
                baseRepo.deleteById(id);//2.删除数据库文件记录
                log.info(sysFile.getBucketName() + ",BucketName=" + sysFile.getUid() + "." + sysFile.getExt() + "文件名删除成功");
            } else {
                log.error(sysFile.getBucketName() + ",BucketName=" + sysFile.getUid() + "." + sysFile.getExt() + "删除失败，可能参数有误或文件不存在!");
                throw new BusinessException(sysFile.getBucketName() + ",BucketName=" + sysFile.getName() + "删除失败，可能参数有误或文件不存在!");
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new BusinessException(e.getMessage());
        }
    }

    @Override
    public String save(SysFile sysFile) {
        if (StringUtils.isBlank(sysFile.getBucketName())) {
            sysFile.setBucketName(minIoClientConfig.getBucketName());
        }
        sysFile = baseRepo.save(sysFile);
        return sysFile.getId();
    }

    @Override
    public SysFile findOne(String id) {
        return baseRepo.findOne(id);
    }

    @Override
    public void deleteFileByFormAndPart(String form, String part) {
        List<SysFile> sysFileList = baseRepo.findAllByFormAndPart(form, part);
        if (!CollectionUtils.isEmpty(sysFileList)) {
            sysFileList.forEach(sysFile -> {
                try {
                    Object obj = this.doRemoveObjects(sysFile.getBucketName(), Arrays.asList(sysFile.getUid() + "." + sysFile.getExt()));
                    if (obj != null) {
                        baseRepo.deleteById(sysFile.getId());//2.删除数据库文件记录
                        log.info(sysFile.getBucketName() + ",BucketName=" + sysFile.getUid() + "." + sysFile.getExt() + "文件名删除成功");
                    } else {
                        log.error(sysFile.getBucketName() + ",BucketName=" + sysFile.getUid() + "." + sysFile.getExt() + "删除失败，可能参数有误或文件不存在!");
                        throw new BusinessException(sysFile.getBucketName() + ",BucketName=" + sysFile.getName() + "删除失败，可能参数有误或文件不存在!");
                    }
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                    throw new BusinessException(e.getMessage());
                }
            });
        }

    }

    @Override
    public String preview(String id) {
        SysFile sysFile = baseRepo.findOne(id);
        if (StringUtils.isEmpty(sysFile)) {
            throw new BusinessException("未找到id:" + id + "数据");
        }

        String url = null;
        try {
            url = minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(sysFile.getBucketName())
                    .object(sysFile.getUid().concat(".").concat(sysFile.getExt()))
                    .expiry(2, TimeUnit.MINUTES)
                    .build());
        } catch (ErrorResponseException e) {
           log.error("请求失败：", e.getMessage());
        } catch (InsufficientDataException e) {
            log.error("通信错误：", e.getMessage());
        } catch (InternalException e) {
            log.error("内部错误：", e.getMessage());
        } catch (InvalidKeyException e) {
            log.error("Key错误：", e.getMessage());
        } catch (InvalidResponseException e) {
            log.error("请求错误：", e.getMessage());
        } catch (IOException e) {
            log.error("IO错误：", e.getMessage());
        } catch (NoSuchAlgorithmException e) {
            log.error("无此算法：", e.getMessage());
        } catch (XmlParserException e) {
            log.error("xml解析失败：", e.getMessage());
        } catch (ServerException e) {
            log.error("服务错误：", e.getMessage());
        }

        return url;
    }

    @Override
    public List<SysFile> getFileByModuleAndForm(String module, String form, String part) {
        if (StringUtils.isBlank(form)) {
            return new ArrayList<>();
        }
        SysFile sysFile = new SysFile();
        sysFile.setModule(module);
        sysFile.setForm(form);
        sysFile.setPart(part);
        List<SysFile> fileList = baseMapper.getFileByModuleAndForm(sysFile);
        return fileList;
    }

    @Override
    public Page<SysFile> listPage(SysFileParam param, PageRequest pageRequest) {
        Page<SysFile> page = baseMapper.listPage(param, pageRequest);
        return page;
    }

    @Override
    public Map<String, String> previewBatch(List<String> idList) throws Exception {
        ParamUtils.throwIfAnyEmpty(idList);

        Map<String, String> resultMap = Maps.newHashMap();
        for (String id : idList) {
            resultMap.put(id, preview(id));
        }
        return resultMap;
    }

    private HttpServletResponse createHeader(String realName) {
        ServletRequestAttributes attributes = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes());
        AssertUtils.notNull(attributes);
        HttpServletRequest request = attributes.getRequest();
        HttpServletResponse response = attributes.getResponse();
        AssertUtils.notNull(response);
        //  建立响应头
        response.setCharacterEncoding("UTF-8");
        ServletContext context = request.getServletContext();
        String mimeType = context.getMimeType(realName);
        if (StringUtils.isEmpty(realName)) {
            realName = "打包文件.zip";
        }
        if (mimeType == null) {
            mimeType = "application/octet-stream";
        }
        response.setContentType(mimeType);
        String headerKey = "Content-Disposition";
        String headerValue;
        try {
            headerValue = "attachment;filename=" + new String(realName.getBytes("gb2312"), "ISO8859-1");
            response.setHeader(headerKey, headerValue);
        } catch (UnsupportedEncodingException e) {
            throw new BusinessException("服务器文件打包异常,请联系管理员!");
        }
        return response;
    }


    /**
     * 上传文件
     *
     * @param sysFile
     * @throws Exception
     */
    public SysFile doUpload(SysFileDto sysFile) throws Exception {
        if (StringUtils.isBlank(sysFile.getBucketName())) {
            sysFile.setBucketName(minIoClientConfig.getBucketName());
        }
        String uuid = UuidUtils.generateUuid().replace("-", "");
        String fileName = uuid;
        // 桶里面创建文件夹
        if (StringUtils.isNotBlank(sysFile.getFilePath())) {
            fileName = sysFile.getFilePath();
        }
        PutObjectArgs objectArgs;
        if (StringUtils.isNotEmpty(sysFile.getMultipartFile())) {
            MultipartFile file = sysFile.getMultipartFile();
            try (InputStream stream = file.getInputStream()) {

                objectArgs = PutObjectArgs.builder().bucket(sysFile.getBucketName()).object(fileName + "." + sysFile.getExt())
                        .stream(stream, file.getSize(), -1).contentType(file.getContentType()).build();
                minioClient.putObject(objectArgs);
            }
        } else {
            try (InputStream is = new ByteArrayInputStream(sysFile.getData())) {
                objectArgs = PutObjectArgs.builder().bucket(sysFile.getBucketName()).object(fileName + "." + sysFile.getExt())
                        .stream(is, is.available(), -1).build();
                minioClient.putObject(objectArgs);
            }
        }
        sysFile.setUid(uuid);
        sysFile.setFilePath(sysFile.getBucketName() + "/" + fileName);

        SysFile ret = new SysFile();
        BeanUtils.copyProperties(sysFile, ret);

        return ret;
    }

    /**
     * 批量删除文件对象
     *
     * @param bucketName 存储bucket名称
     * @param objects    对象名称集合
     */
    public Iterable<Result<DeleteError>> doRemoveObjects(String bucketName, List<String> objects) {
        List<DeleteObject> dos = objects.stream().map(e -> new DeleteObject(e)).collect(Collectors.toList());
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(dos).build());
        return results;
    }

    public void deleteFileObject(SysFile sysFile) {
        try {
            Object obj = this.doRemoveObjects(sysFile.getBucketName(), Arrays.asList(sysFile.getUid() + "." + sysFile.getExt()));
            if (obj != null) {
                log.info(sysFile.getBucketName() + ",BucketName=" + sysFile.getUid() + "." + sysFile.getExt() + "文件名删除成功");
            } else {
                log.error(sysFile.getBucketName() + ",BucketName=" + sysFile.getUid() + "." + sysFile.getExt() + "删除失败，可能参数有误或文件不存在!");
                throw new BusinessException(sysFile.getBucketName() + ",BucketName=" + sysFile.getName() + "删除失败，可能参数有误或文件不存在!");
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new BusinessException(e.getMessage());
        }
    }

    /**
     * 上传分片文件到minio
     * @param file 分片文件
     * @param chunkVo 分片信息
     * @return 文件名称
     */
    @Override
    public String uploadChunk(MultipartFile file, ChunkVo chunkVo) throws Exception {
        String identifier = chunkVo.getIdentifier();
        String total = chunkVo.getTotalChunks();
        String chunkNumber = chunkVo.getChunkNumber();

        SysFileDto sysFileDto = SysFileDto.builder();
        sysFileDto.setMultipartFile(file);

        // 有分片的文件才需要合并， 没有分片的不需要合并
        if (Integer.parseInt(total) > 1) {
            // 每个文件不一样的桶
            sysFileDto.setFilePath(identifier.concat("/").concat(chunkNumber));
        } else {
            String ext = chunkVo.getFilename().substring(chunkVo.getFilename().lastIndexOf(".")+1);
            sysFileDto.setExt(ext);
        }
        // 上传文件
        SysFile ret = doUpload(sysFileDto);

        return ret.getUid();
    }

    @Override
    public String mergeChunks(ChunkMergeVo mergeFileVueVo, ChunkVo chunkVo) {
        String identifier = chunkVo.getIdentifier();
        String bucketName = minIoClientConfig.getBucketName();
        String total = chunkVo.getTotalChunks();
        String ext = mergeFileVueVo.getFilename().substring(mergeFileVueVo.getFilename().lastIndexOf(".")+1);
        String uuid;
        // 查找
        List<ComposeSource> sourceObjectList = new ArrayList<>();

        // 合并
        if (Integer.parseInt(total) > 1) {
            uuid = UuidUtils.generateUuid().replace("-", "");
            try {
                List<String> fileNames = getFolderList(bucketName, identifier);
                for (String name : fileNames) {
                    sourceObjectList.add(ComposeSource.builder().bucket(bucketName).object(name).build());
                }
                // 合并文件
                minioClient.composeObject(
                        ComposeObjectArgs.builder()
                                .bucket(bucketName)
                                .object(uuid.concat(".").concat(ext))
                                .sources(sourceObjectList)
                                .build());
                // 删除分片文件
                for (String name : fileNames) {
                    minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(name).build());
                }
            } catch (Exception e) {
                throw new BusinessException("合并文件异常");
            }
        } else {
            uuid = mergeFileVueVo.getUid();
        }

        // 入库
        SysFile params = new SysFile();
        params.setName(mergeFileVueVo.getFilename());
        params.setExt(ext);
        params.setSize(Long.valueOf(chunkVo.getTotalSize()));
        params.setModule(mergeFileVueVo.getModule());
        params.setPart(mergeFileVueVo.getPart());
        params.setForm(mergeFileVueVo.getForm());
        params.setUid(uuid);
        params.setFilePath(bucketName + "/" + uuid);

        return save(params);
    }

    public List<String> getFolderList(String bucketName, String identifier) throws Exception {
        Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder()
                .bucket(bucketName)
                .prefix(identifier.concat("/"))
                .build());
        Iterator<Result<Item>> iterator = results.iterator();
        List<String> items = new ArrayList<>();
        while (iterator.hasNext()) {
            Item item = iterator.next().get();
            items.add(item.objectName());
        }
        return items;
    }

}
